import concur = module("concur");

interface TestTimeout {
    fn: () =>void;
    when: number;
    token: number;
}

class MockTimer implements concur.Timer {
    private now: number = 0;
    private speed: number = 0;
    private timeouts: TestTimeout[] = [];
    private nextToken: number = 1;

    setTime(time: number): void {
        if( time > this.now )
            this.now = time;
        this.fire();
    }

    setSpeed(speed: number): void {
        this.speed = speed;
    }

    tick(): void {
        this.now += this.speed;
        this.fire();
    }

    currentTime(): number {
        return this.now += this.speed;
    }

    fire(): void {
        while (this.timeouts.length > 0) {
            var timeout = this.timeouts[0];
            if (timeout.when > this.now)
                break;
            this.timeouts.shift();
            timeout.fn();
        }
    }

    setTimeout(fn: () =>void , delay: number): number {
        var token = this.nextToken++;
        var timeout = {
            fn: fn,
            when: this.now + Math.max(delay, 1),
            token: token
        };
        var i: number = this.timeouts.length;
        while (i > 0 && this.timeouts[i - 1].when > timeout.when) {
            i--;
        }
        this.timeouts.splice(i, 0, timeout);
        return token;
    }

    clearTimeout(token: number): void {
        for (var i = 0 ; i < this.timeouts.length ; i++) {
            if (this.timeouts[i].token == token) {
                this.timeouts.splice(i, 1);
                break;
            }
        }
    }

    log(msg: string): void {
        if( window.console && window.console.log )
            window.console.log(msg);
    }
}

class Test {
    failures: number;

    constructor (public className: string) {
    }

    log(msg: string): void {
        if( window.console && window.console.log )
            window.console.log(msg);
    }

    fail(msg: string): void {
        this.log("Failed test in " + this.className + " " + msg);
        this.failures++;
    }

    assertTrue(msg: string, actual: bool): void {
        if (!actual)
            this.fail("assertTrue: " + msg);
    }

    assertFalse(msg: string, actual: bool): void {
        if (actual)
            this.fail("assertFalse: " + msg);
    }

    assertEquals(msg: string, expected: any, actual: any): void {
        if (expected != actual)
            this.fail("assertEquals: " + msg + ", expected " + expected + ", got " + actual);
    }

    assertSame(msg: string, expected: any, actual: any): void {
        if (expected !== actual)
            this.fail("assertSame: " + msg + ", expected " + expected + ", got " + actual);
    }

    assertNotSame(msg: string, expected: any, actual: any): void {
        if (expected === actual)
            this.fail("assertNotSame: " + msg + ", expected not " + expected);
    }

    assertNull(msg: string, actual: Object): void {
        if (actual !== null)
            this.fail("assertNull: " + msg + ", got " + actual);
    }

    assertNotNull(msg: string, actual: Object): void {
        if (actual === null)
            this.fail("assertNotNull: " + msg);
    }

    runTests():void {
        var sync: string[] = [];
        var async: string[] = [];
        var i: number;
        for (var name in this) {
            if (name.match(/^test/)) {
                if( name.match("/Async$/") )
                    async.push(name);
                else
                    sync.push(name);
            }
        }
        for (i = 0 ; i < sync.length ; i++) {
            this.failures = 0;
            var name = sync[i];
            try {
                this[name]();
            } catch (err) {
                this.log("EXCEPTION: " + err);
                this.failures++;
            }
            if( this.failures )
                this.log("FAIL: " + this.className + "." + name);
            else
                this.log("PASS: " + this.className + "." + name);
        }
    }
}

function runTests(testClass: new () => Test): void {
    var t = new testClass();
    t.runTests();
}

// need to test our mock timer
class TimerTest extends Test {
    buffer: string;

    constructor () {
        super("TimerTest");
    }

    append(msg: string): void {
        this.buffer += msg;
    }

    testTimerBasic(): void {
        var timer: MockTimer = new MockTimer();
        this.buffer = "";
        timer.setTime(100);
        var t1 = timer.setTimeout(() =>this.append("1"), 1);
        var t2 = timer.setTimeout(() =>this.append("2"), 2);
        var t3 = timer.setTimeout(() =>this.append("3"), 3);
        var t4 = timer.setTimeout(() =>this.append("4"), 4);
        var t5 = timer.setTimeout(() =>this.append("5"), 5);
        var t6 = timer.setTimeout(() =>this.append("6"), 6);
        var t9 = timer.setTimeout(() =>this.append("9"), 9);
        var t8 = timer.setTimeout(() =>this.append("8"), 8);
        var t7 = timer.setTimeout(() =>this.append("7"), 7);
        timer.clearTimeout(t2);
        timer.clearTimeout(t5);
        timer.clearTimeout(t8);
        timer.setTime(103);
        this.assertEquals("this.buffer", "13", this.buffer);
        timer.setTime(106);
        this.assertEquals("this.buffer", "1346", this.buffer);
        timer.setTime(109);
        this.assertEquals("this.buffer", "134679", this.buffer);
    }

    testTimerInner(): void {
        var timer: MockTimer = new MockTimer();
        this.buffer = "";
        timer.setTime(100);
        var t2 = timer.setTimeout(() =>this.append("2"), 2);
        var t3 = timer.setTimeout(() =>this.append("3"), 3);
        var t4 = timer.setTimeout(() =>this.append("4"), 4);
        var t1 = timer.setTimeout(() =>{
            this.append("1");
            timer.setTimeout(() =>this.append("5"), 1);
            timer.clearTimeout(t3);
        }, 1);
        timer.setTime(104);
        this.assertEquals("this.buffer", "124", this.buffer);        
        timer.setTime(105);
        this.assertEquals("this.buffer", "1245", this.buffer);        
    }
}

class BasicTaskTest extends Test {
    buffer: string;

    constructor () {
        super("BasicTaskTest");
    }

    append(msg: string): void {
        this.buffer += msg;
    }

    loop(count:number, out:string): concur.Result {
        var frame = concur.newFrame("BasicTaskTest.loop");
        frame.loop((): concur.Result =>{
            if( count-- == 0 )
                return null;
            this.append(out);
            return frame.timeSlice();
        }).then(() =>frame.finish());
        return frame.result();
    }

    raiseAsync(err: string): concur.Result {
        var frame = concur.newFrame("BasicTaskTest.loop");
        frame.timeSlice().then(() =>frame.raise(err));
        return frame.result();
    }

    testRoundRobin(): void {
        var timer: MockTimer = new MockTimer();
        timer.setSpeed(1000);
        var scheduler = concur.newScheduler(timer);
        this.buffer = "";
        timer.setTime(1000);
        scheduler.run(() =>this.loop(3, "A")).whenDone(() =>this.append("1"));
        scheduler.run(() =>this.loop(3, "B")).whenDone(() =>this.append("2"));
        scheduler.run(() =>this.loop(3, "C")).whenDone(() =>this.append("3"));
        for (var i = 0 ; i < 20 ; i++)
            timer.tick();
        this.assertEquals("this.buffer", "ABCABCABC123", this.buffer);        
    }

    testException(): void {
        var timer: MockTimer = new MockTimer();
        timer.setSpeed(1000);
        var scheduler = concur.newScheduler(timer);
        this.buffer = "";
        timer.setTime(1000);
        scheduler.run(():concur.Result =>{
            var frame = concur.newFrame("top");
            this.loop(3, "S").thenAsync(() =>this.raiseAsync("E")).then(() =>{
                try {
                    frame.check();
                    this.append("A");
                } catch (err) {
                    this.append("B" + err.message);
                }
                frame.finish();
            });
            return frame.result();
        });
        for (var i = 0 ; i < 20 ; i++)
            timer.tick();
        this.assertEquals("this.buffer", "SSSBE", this.buffer);        
    }

    testInterrupt(): void {
        var timer: MockTimer = new MockTimer();
        timer.setSpeed(1000);
        var scheduler = concur.newScheduler(timer);
        this.buffer = "";
        timer.setTime(1000);
        var task = scheduler.run((): concur.Result =>{
            var frame = concur.newFrame("top");
            this.loop(10, "S").then(() =>{
                try {
                    frame.check();
                    this.append("A");
                } catch (err) {
                    if (err.message == "XXX")
                        this.append("C");
                    else
                        this.append("B");
                }
                frame.finish();
            });
            return frame.result();
        });
        this.assertSame("scheduler", scheduler, task.getScheduler());
        var armed = true;
        for (var i = 0 ; i < 20 ; i++) {
            timer.tick();
            if (armed && this.buffer.length >= 3) {
                armed = false;
                task.interrupt("XXX");
            }
        }
        this.assertEquals("this.buffer", "SSSC", this.buffer);
    }

}

function main() {
    runTests(TimerTest);
    runTests(BasicTaskTest);
}

if(window["__loaded"])
    main();
else
    window.onload = main;

